﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Xml.Linq;
using YawetagLib.Utilities;

namespace YawetagLib.Data.Xml
{
    public sealed class DataSource<TEntity> : IDataSource<TEntity> where TEntity : class
    {
        public DataSource(DataContext dataContext)
        {
            DataContext = dataContext;
        }

        public TEntity Get(int entityID)
        {
            return GetEntity<TEntity>(GetElements<TEntity>(i => i.GetElementID<TEntity>() == entityID).Single());
        }

        public TEntity GetOrDefault(int entityID)
        {
            XElement element = GetElements<TEntity>(i => i.GetElementID<TEntity>() == entityID).SingleOrDefault();

            return element != null ? GetEntity<TEntity>(element) : null;
        }

        public IEnumerable<TEntity> GetAll()
        {
            return GetElements<TEntity>().Select(GetEntity<TEntity>);
        }

        public IList<TEntity> GetWhere(Expression<Func<TEntity, bool>> predicate)
        {
            return GetElements<TEntity>(predicate.ToXPredicate()).Select(GetEntity<TEntity>).ToList();
        }

        public void Insert(TEntity entity)
        {
            entity.SetEntityID(GetNextEntityID());
            GetContainerElement().Add(CreateElement(entity));
        }

        public void Update(TEntity entity)
        {
            UpdateElement(GetElement(entity), entity);
        }

        public void Delete(TEntity entity)
        {
            GetElement(entity).Remove();
        }

        private XElement GetContainerElement()
        {
            return GetElements(typeof(TEntity).GetXEntityAttribute().ContainerName).First();
        }

        private int GetNextEntityID()
        {
            XElement element = GetElements<TEntity>().LastOrDefault();

            return element != null ? element.GetElementID<TEntity>() + 1 : 1;
        }

        private static void UpdateElement(XElement element, TEntity entity)
        {
            typeof(TEntity).ForEachPropertyInfo(p => element.SetAttibuteAt(p.Name, entity.GetAt(p.Name)));
        }

        private IEnumerable<XElement> GetElements(string elementName)
        {
            return Document.Descendants(elementName);
        }

        private IEnumerable<XElement> GetElements<TEntity2>()
        {
            return GetElements(typeof(TEntity2).GetXEntityAttribute().ElementName);
        }

        private IEnumerable<XElement> GetElements<TEntity2>(Func<XElement, bool> predicate)
        {
            return GetElements<TEntity2>().Where(predicate);
        }

        private TEntity2 GetEntity<TEntity2>(XElement element)
        {
            string referenceKey = typeof(TEntity2).GetEntityReferenceKey(element.GetElementID<TEntity2>());

            if (!References.ContainsKey(referenceKey))
            {
                TEntity2 entity = XProxyBuilder.CreateInstance<TEntity2>(GetEntityReference);
                IEnumerable<PropertyInfo> propertiesInfo = typeof (TEntity2).GetProperties();
                propertiesInfo.Where(i => i.IsXFieldProperty()).ForEachPropertyInfo(i => SetEntityField(element, entity, i));
                propertiesInfo.Where(i => i.IsXReferenceProperty()).ForEachPropertyInfo(i => SetReferenceField(entity, i));

                References.Add(referenceKey, entity);

                return entity;
            }

            return (TEntity2)References[referenceKey];
        }

        private static void SetEntityField(XElement element, object entity, PropertyInfo propertyInfo)
        {
            propertyInfo.SetValue(entity, element.GetAttibuteAt(propertyInfo.Name, propertyInfo.PropertyType), null);
        }

        private void SetReferenceField(object entity, PropertyInfo propertyInfo)
        {
            if (!propertyInfo.GetXReferenceAttribute().IsLasyLoading) 
            {
                propertyInfo.SetValue(entity, GetEntityReference(entity, propertyInfo.Name), null);
            }
        }

        private object GetEntityReference(object entity, string propertyName)
        {
            XEntityAttribute entityAttribute = entity.GetType().BaseType.GetXEntityAttribute();
            XReferenceAttribute referenceAttribute = entity.GetType().BaseType.GetProperty(propertyName).GetXReferenceAttribute();

            Type entityType = entityAttribute.EntityType;
            Type referencedEntityType = referenceAttribute.ReferencedEntityType;
            string foreignKeyName = referenceAttribute.ForeignKeyName;

            return referenceAttribute.ReferenceType == XReferenceType.OneToOne
                ? GetPrimaryEntity(referencedEntityType, entityType, entity, foreignKeyName)
                : GetForeingEntities(entityType, referencedEntityType, entity, foreignKeyName);
        }

        private XElement GetElement<TEntity2>(TEntity2 entity)
        {
            int entityID = entity.GetEntityID();

            return GetElements<TEntity2>(i => i.GetElementID<TEntity2>() == entityID).Single();
        }

        private static XElement CreateElement<TEntity2>(TEntity2 entity)
        {
            IList<XAttribute> attributes = new List<XAttribute>();
            typeof(TEntity2).ForEachPropertyInfo(p => attributes.Add(new XAttribute(p.Name, entity.GetAt(p.Name))));
            XElement element = new XElement(typeof(TEntity2).GetXEntityAttribute().ElementName, attributes);

            return element;
        }

        private IEnumerable<XElement> GetForeingElements<TPrimary, TForeign>(TPrimary primary, string foreignKeyName)
        {
            int primaryKey = primary.GetEntityID();

            return GetElements<TForeign>(i => i.GetElementKey(foreignKeyName) == primaryKey);
        }

        public IList<TForeign> GetForeingEntities<TPrimary, TForeign>(TPrimary primary, string foreignKeyName)
        {
            return GetForeingElements<TPrimary, TForeign>(primary, foreignKeyName).Select(GetEntity<TForeign>).ToList();
        }

        private object GetForeingEntities(Type primaryType, Type foreignType, object primary, string foreignKeyName)
        {
            return GetForeingEntitiesMethodInfo()
                .MakeGenericMethod(new[] { primaryType, foreignType })
                .Invoke(this, new[] { primary, foreignKeyName });
        }

        private MethodInfo GetForeingEntitiesMethodInfo() 
        {
            return GetType().GetMethods()
                .Where(i => i.IsGenericMethod && i.Name == "GetForeingEntities")
                .Single();
        }

        private XElement GetPrimaryElement<TPrimary, TForeign>(TForeign foreign, string foreignKeyName)
        {
            int foreingKey = foreign.GetAt<int>(foreignKeyName);

            return GetElements<TPrimary>(i => i.GetElementID<TPrimary>() == foreingKey).Single();
        }

        public TPrimary GetPrimaryEntity<TPrimary, TForeign>(TForeign foreign, string foreignKeyName)
        {
            return GetEntity<TPrimary>(GetPrimaryElement<TPrimary, TForeign>(foreign, foreignKeyName));
        }

        private object GetPrimaryEntity(Type primaryType, Type foreignType, object foreign, string foreignKeyName)
        {
            return GetPrimaryEntityMethodInfo()
                .MakeGenericMethod(new[] { primaryType, foreignType })
                .Invoke(this, new[] { foreign, foreignKeyName });
        }

        private MethodInfo GetPrimaryEntityMethodInfo()
        {
            return GetType().GetMethods()
                .Where(i => i.IsGenericMethod && i.Name == "GetPrimaryEntity")
                .Single();
        }

        private DataContext DataContext { get; set; }

        private XDocument Document
        {
            get { return DataContext.Document; }
        }

        private IDictionary<string, object> References
        {
            get { return DataContext.References; }
        }
    }
}
